Clasificador - hito 2¶

In [ ]:
import pandas as pd
import numpy as np
import plotly.graph_objects as go
from config import *

# mas imports
from nltk.tokenize import TweetTokenizer
tt = TweetTokenizer()

cargar sets

In [ ]:
df_es_train = pickle.load(open(file_names["df_es_train"], "rb"))
df_es_trial = pickle.load(open(file_names["df_es_trial"], "rb"))
df_es_test = pickle.load(open(file_names["df_es_test"], "rb"))

pre-procesamiento

In [ ]:
%%time
df_es_train['tokenized_text'] = df_es_train['text'].str.lower().apply(lambda x: " ".join(tt.tokenize(x)))
df_es_train.head()
CPU times: user 3.62 s, sys: 121 ms, total: 3.74 s
Wall time: 3.77 s
Out[ ]:
id text label tokenized_text
0 793417168469757952 Es imposible quererte más @ Plaza Del Callao -... 0 es imposible quererte más @ plaza del callao -...
1 718539939063926790 Disfrutando de buena comida con buena compañía... 4 disfrutando de buena comida con buena compañía...
2 670562346067193856 Muchísimas Felicidades M!!! Nos vemos pronto! ... 11 muchísimas felicidades m ! ! ! nos vemos pront...
3 783680728538214400 Y pensar que a esta persona la conozco de hace... 11 y pensar que a esta persona la conozco de hace...
4 711210617043075073 ¡Que buenas son las noches así y que buena com... 17 ¡ que buenas son las noches así y que buena co...
In [ ]:
df_es_test['tokenized_text'] = df_es_test['text'].str.lower().apply(lambda x: " ".join(tt.tokenize(x)))
In [ ]:
from sklearn.feature_extraction.text import CountVectorizer

vectorizer = CountVectorizer(min_df=5)
X_train_bow = vectorizer.fit_transform(df_es_train["tokenized_text"])
X_test_bow = vectorizer.transform(df_es_test["tokenized_text"])
In [ ]:
from sklearn.naive_bayes import MultinomialNB

clf = MultinomialNB()
clf.fit(X_train_bow, df_es_train["label"]);
In [ ]:
clf.score(X_train_bow, df_es_train["label"])
Out[ ]:
0.3847601013206109
In [ ]:
from sklearn.metrics import classification_report

df_es_mapping = pickle.load(open(file_names["df_es_mapping"], "rb")).sort_values("label")

y_pred = clf.predict(X_test_bow)
print(classification_report(df_es_test["label"], y_pred, target_names=df_es_mapping["emoji"]))
              precision    recall  f1-score   support

           ❤       0.33      0.52      0.41      2141
           😍       0.27      0.34      0.30      1408
           😎       0.16      0.07      0.10       339
           💙       0.18      0.01      0.02       413
           💜       0.00      0.00      0.00       235
           😜       0.14      0.02      0.04       274
           💞       0.00      0.00      0.00        93
           ✨       0.21      0.04      0.06       416
           🎶       0.16      0.11      0.13       212
           💘       0.00      0.00      0.00       134
           😁       0.03      0.00      0.01       209
           😂       0.41      0.63      0.50      1499
           💕       0.06      0.03      0.04       352
           😊       0.10      0.12      0.11       514
           😘       0.21      0.10      0.14       397
           💪       0.36      0.34      0.35       307
           😉       0.11      0.05      0.07       453
           👌       0.14      0.07      0.10       180
          🇪🇸       0.32      0.39      0.36       424

    accuracy                           0.30     10000
   macro avg       0.17      0.15      0.14     10000
weighted avg       0.25      0.30      0.26     10000

In [ ]:
vocab = {k: v for v, k in enumerate(vectorizer.get_feature_names_out())}

vec_test = np.zeros(X_train_bow.shape[1])
k = vocab["españa"]
vec_test[k] = 1
print(vectorizer.inverse_transform([vec_test])[0][0])
clf.predict_proba([vec_test])
españa
Out[ ]:
array([[0.21389077, 0.14984582, 0.02407798, 0.02703996, 0.01742892,
        0.01001667, 0.01166575, 0.01880478, 0.01127802, 0.01410191,
        0.01677726, 0.04141457, 0.04917007, 0.04752564, 0.02019323,
        0.01585438, 0.01578489, 0.02303667, 0.27209274]])

GridSearch

In [ ]:
%%capture

from sklearn.metrics import f1_score, accuracy_score

dfs = [1,2,3,4,5,6,7,8,9,10]
alphas = [0, 0.2, 0.4, 0.6, 0.8, 1]

f1_m = {}
f1_w = {}
scores={}

for j in alphas:
    f1_m[j] = {}   
    f1_w[j] = {}
    scores[j] = {}
    for i in dfs:
        #Vectorización 
        vectorizer = CountVectorizer(min_df=i) #Definimos la cantidad de veces que se repite una palabra para que el clasificador la tome en consideración.
        X_train_bow = vectorizer.fit_transform(df_es_train["tokenized_text"])
        X_test_bow = vectorizer.transform(df_es_test["tokenized_text"])

        #Obtenemos el clf score para el clasificador
        clf = MultinomialNB(alpha=j)
        clf.fit(X_train_bow, df_es_train["label"])
        clf.score(X_train_bow, df_es_train["label"])

        y_pred = clf.predict(X_test_bow)
        y_true = df_es_test["label"]
        
        # Se llenan los diccionarios
        f1_m[j][i] = f1_score(y_true, y_pred, average="macro")
        f1_w[j][i] = f1_score(y_true, y_pred, average="weighted")   
        scores[j][i] = accuracy_score(y_true, y_pred)
In [ ]:
scores_array = []
for i in scores:

    data = np.array(list(scores[i].values()) ).reshape(len(dfs),)
    scores_array.append(data)
In [ ]:
x_axis_labels = []

plt.title("Accuracy del clasificador para distintos parámetros")
sns.heatmap(scores_array, annot=True, xticklabels=dfs, yticklabels=alphas)
plt.xlabel('minimas ocurrencias para token')
plt.ylabel('alpha');
In [ ]:
f1_m_array = []

for i in f1_m:

    data = np.array(list(f1_m[i].values()) ).reshape(len(dfs),) # transform to 2D
    f1_m_array.append(data)

f1_m_array

plt.title("Macro-f1 del clasificador para distintos parámetros")
sns.heatmap(f1_m_array, annot=True, xticklabels=dfs, yticklabels=alphas)
plt.xlabel('minimas ocurrencias para token')
plt.ylabel('alpha');
In [ ]:
f1_w_array = []

for i in f1_w:
    data = np.array(list(f1_w[i].values()) ).reshape(len(dfs),) # transform to 2D
    f1_w_array.append(data)

f1_w_array

plt.title("Weighted-f1 del clasificador para distintos parámetros")
sns.heatmap(f1_w_array, annot=True, xticklabels=dfs, yticklabels=alphas)
plt.xlabel('minimas ocurrencias para token')
plt.ylabel('alpha');

Finalmente, como resulado de este grid-search, escogemos el clasificador con el mejor macro-f1.

In [ ]:
best_alpha, best_min_df = np.unravel_index(np.argmax(np.array(f1_m_array)),shape=(len(alphas),len(dfs)))

scr, macf1, weif1 = scores_array[best_alpha][best_min_df], f1_m_array[best_alpha][best_min_df], f1_w_array[best_alpha][best_min_df]
best_alpha, best_min_df = alphas[best_alpha], dfs[best_min_df]

print("Parámetros escogidos:\n\talpha = {}\n\tminimas ocurrencias para token = {}".format(best_alpha,best_min_df))
print("Resultados de clasificación:\n\taccuracy = {}\n\tmacro f1 = {}\n\tweighted f1 = {}".format(scr,macf1,weif1))
Parámetros escogidos:
	alpha = 0.2
	minimas ocurrencias para token = 5
Resultados de clasificación:
	accuracy = 0.2735
	macro f1 = 0.1592299196449812
	weighted f1 = 0.2585226651076328
In [ ]:
vectorizer = CountVectorizer(min_df=best_min_df)
X_train_bow = vectorizer.fit_transform(df_es_train["tokenized_text"])
X_test_bow = vectorizer.transform(df_es_test["tokenized_text"])

clf = MultinomialNB(alpha=best_alpha)
clf.fit(X_train_bow, df_es_train["label"])

y_pred = clf.predict(X_test_bow)
print(classification_report(df_es_test["label"], y_pred, target_names=df_es_mapping["emoji"]))
              precision    recall  f1-score   support

           ❤       0.36      0.39      0.37      2141
           😍       0.28      0.30      0.29      1408
           😎       0.13      0.11      0.12       339
           💙       0.13      0.04      0.06       413
           💜       0.10      0.03      0.05       235
           😜       0.11      0.06      0.08       274
           💞       0.00      0.00      0.00        93
           ✨       0.22      0.11      0.14       416
           🎶       0.13      0.19      0.16       212
           💘       0.05      0.02      0.03       134
           😁       0.05      0.04      0.04       209
           😂       0.44      0.55      0.49      1499
           💕       0.05      0.04      0.04       352
           😊       0.11      0.14      0.12       514
           😘       0.20      0.16      0.18       397
           💪       0.28      0.36      0.32       307
           😉       0.11      0.09      0.10       453
           👌       0.08      0.11      0.10       180
          🇪🇸       0.29      0.41      0.34       424

    accuracy                           0.27     10000
   macro avg       0.16      0.17      0.16     10000
weighted avg       0.25      0.27      0.26     10000

Top palabras por emoji¶

In [ ]:
%%time
vocab_length = X_train_bow.shape[1]
proba_matrix = np.array([clf.predict_proba(np.eye(1,vocab_length,k))[0] for k in range(vocab_length)])
CPU times: user 8.75 s, sys: 176 ms, total: 8.93 s
Wall time: 1.5 s
In [ ]:
print("Largo del vocabulario = {}".format(vocab_length))
Largo del vocabulario = 10736
In [ ]:
def topPalabras(proba_matrix,emoji_id,k=5):
    # retorna las palabras para las cuales el emoji en cuestión tiene mas probabilidad
    prob = proba_matrix[:,emoji_id]  # mmm
    ind = np.argpartition(prob,-k)[-k:]
    val = prob[ind]
    palabras = [vectorizer.inverse_transform([np.eye(1,vocab_length,k)[0]])[0][0] for k in ind]
    return palabras, val
In [ ]:
i = 9
map_emojis = df_es_mapping["label"].values
print(df_es_mapping["emoji"][int(map_emojis[i])])
topPalabras(proba_matrix,i)
💘
Out[ ]:
(['ciento', 'sorbas', 'felizzzz', 'ordino', 'flechazo'],
 array([0.29971893, 0.35198258, 0.32238495, 0.36437439, 0.42107903]))
In [ ]:
for i in range(19):
    print(df_es_mapping["emoji"][int(map_emojis[i])])
    pal, val = topPalabras(proba_matrix,i)
    print(dict([(pal[j],val[j]) for j in range(len(pal))]))
❤
{'unanoche': 0.6860863379428201, 'belovedsweets': 0.6860863379428201, 'cachetita': 0.7133967002190734, 'ilovemadrid': 0.7399390301272345, 'benidormsunset': 0.6860863379428201}
😍
{'enamorada': 0.5718216196639336, 'maya': 0.6105414759238323, 'amordehermanos': 0.6267603262682746, 'losamo': 0.6846039304610873, 'preciosidad': 0.6432461755919693}
😎
{'rayban': 0.551543379333054, 'sunglasses': 0.5603550633565935, 'óptico': 0.6111180308445737, 'elguay': 0.660673286458433, 'worldwide': 0.6419866378515099}
💙
{'pals': 0.3297575681277473, 'azul': 0.3350016333528502, 'blue': 0.37336488876711676, 'cansaron': 0.48460911180633653, 'madrid2016': 0.4358695856869071}
💜
{'calera': 0.35959464754112863, 'atlantis': 0.3604829252578627, 'unid': 0.5254518517216809, 'magical': 0.48995516539934514, 'lavanda': 0.48150447645265865}
😜
{'gaymen': 0.5090172336899016, 'movember': 0.5376420398163474, 'thebestbarbershop': 0.6509312907435113, 'nopuedoparar': 0.5738833248004648, 'quiquepop': 0.6509312907435113}
💞
{'ochavada': 0.250724674740773, 'facultat': 0.25881681872272144, 'provencio': 0.26598555542213626, 'esenciales': 0.3279198365614017, 'planean': 0.3022318975150827}
✨
{'nacen': 0.34582480610273564, 'oscuridad': 0.404553090869053, 'mágicas': 0.3699446557533326, 'brilla': 0.34975734800470576, 'estrellas': 0.3932700067902005}
🎶
{'baila': 0.4377499392658804, 'cuéntame': 0.5722121226839422, 'mírala': 0.4591353770396685, 'illana': 0.4791939029402562, 'n1canalfiesta11': 0.6515203022570265}
💘
{'ciento': 0.29971892806759853, 'sorbas': 0.35198258019256873, 'felizzzz': 0.3223849548412547, 'ordino': 0.36437439188578064, 'flechazo': 0.4210790291336439}
😁
{'comete': 0.3139798480106664, 'instamoments': 0.33029923776904047, 'turística': 0.34330549396908605, 'bla': 0.3907265887170254, 'pasarse': 0.3497952185560865}
😂
{'jajajajajajajaja': 0.6315007019485125, 'caretos': 0.6666766276910938, 'parto': 0.6806120176160075, 'aburrimiento': 0.7135363112316856, 'meo': 0.8598691073304411}
💕
{'ombligo': 0.5026220206472154, 'perricos': 0.6442557798172189, 'envío': 0.564164644510615, 'gandules': 0.6442557798172189, 'atrapasueños': 0.6030029321507036}
😊
{'askdavidparejo': 0.6048248361695145, 'esmorzant': 0.6256383999389286, 'esperanza_pm': 0.6314582227369102, 'acuaman': 0.6655239709398794, 'momoa': 0.6655239709398794}
😘
{'tzayn': 0.7138583792657064, 'llevara': 0.7138583792657064, 'difusion': 0.7138583792657064, 'ayudaria': 0.7138583792657064, 'votad': 0.7138583792657064}
💪
{'quiendijomiedo': 0.630642214482262, 'wellness': 0.66474334355345, 'gymlife': 0.6649922059812844, 'altafit': 0.6700300262476342, 'empujón': 0.6930798856400404}
😉
{'gelidamenamora': 0.6043236382659811, 'whatsapp650200641': 0.6408912013002867, 'allianz': 0.668874309818772, 'martivell': 0.7339736484582075, 'bohio': 0.7776814868993555}
👌
{'nochaza': 0.3506713791873669, '080': 0.3554983893951407, 'alájar': 0.35574603962497187, 'calité': 0.4555915124593617, 'completito': 0.46869384657874197}
🇪🇸
{'europetrip2015': 0.6776747600931378, 'gaycation': 0.748647717488816, 'hiszpania': 0.748647717488816, 'vivaespaña': 0.8080750922074575, 'hispanidad': 0.7851892152947274}

Visualización de tokens según Naive Bayes¶

Esta seccion consiste en una visualizacion de los tokens segun la codificacion que nos entrega Naive Bayes. De la seccion anterior, se pueden obtener las probabilidad de que un token pertenezca a una clase dada. En nuestro caso, a un emoji dado. Esto es:

$$ P(w \in C) = \frac{\text{\#(tweets donde $w$ es uno de sus tokens y el tweet tiene el emoji $C$)}}{\text{\#(tweets con el token $w$)}} $$

De esta manera, cada token posee un vector de probabilidades. Donde la $C-$esima componente corresponde a $P(w \in C)$. Es decir,

$$\vec{w} = (P(w \in C) : \text{$C$ es un emoji})$$

En particular, cada vector $\vec{w}$ es uno con tantas coordenadas como emojis (20 en Ingles). Y cada coordenada esta entre 0 y 1. Es decir, cada $\vec{w} \in [0, 1]^{\text{\#Emojis}}$.

Ahora bien, es de nuestro interes visualizar cada token segun su vector de probabilidad. Sin embargo, es necesario reducir la dimensionalidad de cada vector a una facil de interpretar (en nuestro caso 2-dimensiones). Para esto, se utiliza un metodo de reduccion de dimensionalidad denominado UMAP y ampliamente utilizado para la visualizacion de datos en altas dimensiones.

In [ ]:
import umap.umap_ as umap
In [ ]:
%%time
reducer = umap.UMAP(n_neighbors=15)
to_R2 = reducer.fit_transform(proba_matrix)
to_R2.shape
OMP: Info #276: omp_set_nested routine deprecated, please use omp_set_max_active_levels instead.
CPU times: user 47.3 s, sys: 688 ms, total: 48 s
Wall time: 16.5 s
Out[ ]:
(10736, 2)

Luego de reducir los vectores de probabilidad a uno de bi-dimensional, visualizaremos segun dos aspectos el espacio de tokens. Primero, se colorean los vectores segun el emoji con mayor probabilidad. Por ejemplo, si el token $happy$ tiene mayor probabilidad de estar en la clase $smile$, entonces se asocia este token con dicho emoji. La razon de esto es solo para simplificar el analisis. Segundo, existen tokens con probabilidades maximas mas grandes que otras, es decir, tokens asociados a un mismo emoji (segun el criterio anterior) que poseen probabilidades distintas de pertenecer a dicha clase. Para observar esto, se visualizan los token con puntos de diferente tamaño y proporcional a tal probabilidad.

In [ ]:
df_umap = pd.DataFrame(to_R2)
df_umap["token"] = vectorizer.get_feature_names_out()
df_umap["label"] = map_emojis[np.argmax(proba_matrix, axis=1).astype(int)]
df_umap["proba"] = np.max(proba_matrix, axis=1)
df_umap = df_umap.merge(df_es_mapping, on="label", how="left")
df_umap
Out[ ]:
0 1 token label proba emoji name
0 3.981075 9.959625 00 2 0.142243 😂 _face_with_tears_of_joy_
1 3.048105 12.807270 000 0 0.267381 ❤ _red_heart_
2 7.093706 9.134564 01 0 0.168436 ❤ _red_heart_
3 4.897144 6.150537 02 0 0.168031 ❤ _red_heart_
4 8.010995 5.631053 03 0 0.376696 ❤ _red_heart_
... ... ... ... ... ... ... ...
10731 7.112108 7.634966 único 0 0.227317 ❤ _red_heart_
10732 7.991124 6.723417 únicos 0 0.295065 ❤ _red_heart_
10733 2.453170 7.819929 スペイン 4 0.364940 😊 _smiling_face_with_smiling_eyes_
10734 2.668646 4.042776 세비야 9 0.602929 🇪🇸 _Spain_
10735 2.669938 4.044263 스페인광장 9 0.602929 🇪🇸 _Spain_

10736 rows × 7 columns

In [ ]:
data = []
for label in df_es_mapping["label"]:
    sub_df = df_umap[df_umap["label"] == label]
    data.append(
        go.Scattergl(
            x = sub_df[0],
            y = sub_df[1],
            mode='markers',
            text=sub_df["token"]+"<br>"+sub_df["emoji"]+"<br>"+sub_df["proba"].apply(lambda x: str(np.round(x, 3))),
            name=sub_df["emoji"].iloc[0],
            marker=dict(
                size=25*sub_df["proba"],
                line_width=0.2,
            )
        )
    )
    
fig = go.Figure(data=data)
fig.update_layout(
    title="Proyección (UMAP) de vectores de probabilidad de tokens",
    autosize=False,
    width=700,
    height=500,
)
fig.show(renderer="notebook")

Comentarios El top 5 de la seccion anterior se puede capturar con los primero cinco punto de mayor tamaño para un emoji dado. Tambien, se observa que la clase con mas puntos corresponde al emoji del corazon. Mismo emoji con mayor popularidad visto en la etapa de analisis de los datos. Se observan grupos diferenciados, pero que logran solaparse. Esta zona coincide con aquellos tokens con probabilidades uniformes de pertenecer a cada clase y/o con probabilidad maxima cercanas a 0.1.

Distribución de clases usando subsampling

In [ ]:
df_es_train1 = pickle.load(open(file_names["df_es_train"], "rb"))
In [ ]:
print("Distribucion de clases original")
counts = df_es_train1['label'].value_counts()
counts
Distribucion de clases original
Out[ ]:
0     16102
1     11429
2      7725
4      5448
3      5348
5      3660
6      3124
7      3117
8      2884
9      2757
10     2610
11     2357
13     2289
12     2211
18     2203
16     2112
14     2041
15     2006
17     1903
Name: label, dtype: int64
In [ ]:
min_freq = np.min(counts.values)
min_class = list(counts.index)[np.argmin(counts.values)]
print("Mínima frecuencia entre las distintas clases = {}\nEmoji class = {}".format(min_class,min_class))
Mínima frecuencia entre las distintas clases = 17
Emoji class = 17
In [ ]:
reduce_index = list(counts.index)
reduce_index.remove(min_class)
In [ ]:
for label in reduce_index:
    freq = counts[label]
    delete_counts = freq - min_freq
    df_es_train1 = df_es_train1.reset_index(drop=True)
    ## subsampling sobre la clase label
    idx = np.random.choice(df_es_train1.loc[df_es_train1.label == label].index, size=delete_counts, replace=False)
    data_subsampled = df_es_train1.drop(df_es_train1.iloc[idx].index, inplace = True)
In [ ]:
%%time
df_es_train1['tokenized_text'] = df_es_train1['text'].str.lower().apply(lambda x: " ".join(tt.tokenize(x)))
df_es_train1.head()
CPU times: user 1.75 s, sys: 7.91 ms, total: 1.75 s
Wall time: 1.76 s
Out[ ]:
id text label tokenized_text
0 670562346067193856 Muchísimas Felicidades M!!! Nos vemos pronto! ... 11 muchísimas felicidades m ! ! ! nos vemos pront...
1 711210617043075073 ¡Que buenas son las noches así y que buena com... 17 ¡ que buenas son las noches así y que buena co...
2 745615686655967232 Un placer haberos conocido @ Colegio Santa Vic... 0 un placer haberos conocido @ colegio santa vic...
3 670341440862560256 @user yo te lo desaburro guapetón 14 @user yo te lo desaburro guapetón
4 665984610812215297 San Martín '15 inmejorables @ Cardeñadijo, Spain 6 san martín ' 15 inmejorables @ cardeñadijo , s...
In [ ]:
df_es_test['tokenized_text'] = df_es_test['text'].str.lower().apply(lambda x: " ".join(tt.tokenize(x)))
In [ ]:
vectorizer = CountVectorizer(min_df=10)
X_train_bow1 = vectorizer.fit_transform(df_es_train1["tokenized_text"])
X_test_bow = vectorizer.transform(df_es_test["tokenized_text"])
clf = MultinomialNB(alpha = .2)
clf.fit(X_train_bow1, df_es_train1["label"])

clf.score(X_train_bow1, df_es_train1["label"])
Out[ ]:
0.34621235168846975
In [ ]:
df_es_mapping = pickle.load(open(file_names["df_es_mapping"], "rb")).sort_values("label")

y_pred = clf.predict(X_test_bow)
print(classification_report(df_es_test["label"], y_pred, target_names=df_es_mapping["emoji"]))
              precision    recall  f1-score   support

           ❤       0.37      0.06      0.11      2141
           😍       0.30      0.11      0.16      1408
           😎       0.10      0.16      0.12       339
           💙       0.09      0.07      0.07       413
           💜       0.05      0.06      0.05       235
           😜       0.04      0.05      0.04       274
           💞       0.01      0.04      0.02        93
           ✨       0.14      0.15      0.14       416
           🎶       0.08      0.27      0.12       212
           💘       0.04      0.10      0.05       134
           😁       0.04      0.09      0.06       209
           😂       0.43      0.33      0.37      1499
           💕       0.05      0.04      0.05       352
           😊       0.09      0.08      0.08       514
           😘       0.16      0.28      0.21       397
           💪       0.22      0.37      0.28       307
           😉       0.09      0.14      0.11       453
           👌       0.05      0.17      0.08       180
          🇪🇸       0.23      0.46      0.31       424

    accuracy                           0.16     10000
   macro avg       0.14      0.16      0.13     10000
weighted avg       0.24      0.16      0.16     10000

Conclusiones

A partir del subsampling, se puede observar que los resultados no presentan una mejorar e incluso en varias clases disminuye la efectividad del clasificador, por lo que es mejor no hacer el subsampling para obtener mejores metricas.